package main

import (
	"fmt"
	"os"
	"strings"
	"time"

	"github.com/labstack/echo/v4"
	"github.com/labstack/echo/v4/middleware"
	"github.com/markkurossi/tabulate"
	"github.com/samber/lo"
	log "github.com/sirupsen/logrus"
)

type AliyunConfigure struct {
	AccessKey string `json:"access_key"`
	SecretKey string `json:"secret_key"`
	Endpoint  string `json:"endpoint"`
	Region    string `json:"region"`
	Bucket    string `json:"bucket"`
}

type Node struct {
	Host     string `json:"host"`
	Port     int    `json:"port"`
	Password string `json:"password"`
	User     string `json:"user"`
	Name     string `json:"name"`
	Prefix   string `json:"prefix"`
}

type Configure struct {
	Command  []string        `json:"command"`
	Database []Node          `json:"database"`
	Key      string          `json:"key"`
	Aliyun   AliyunConfigure `json:"aliyun"`
}

var config Configure
var version string

type TableRow = map[string]string

func OutputTable(head []string, body []TableRow) {
	table := tabulate.New(tabulate.ASCII)
	for _, ele := range head {
		table.Header(ele)
	}
	for _, ele := range body {
		row := table.Row()
		for _, elm := range head {
			val := ele[elm]
			row.Column(val)
		}
	}
	table.Print(os.Stdout)
}

func GetKey(node Node) string {
	key := strings.ReplaceAll(config.Key, "{prefix}", node.Prefix)
	key = strings.ReplaceAll(key, "{today}", time.Now().Format("2006-01-02"))
	key = strings.ReplaceAll(key, "{name}", node.Name)
	return key
}

func BackupMysql(node Node) {
	key := GetKey(node)
	result := MysqlDump(node)
	if result.IsError() {
		return
	}
	aliyun, err := NewAliyunBucketWithConfig(config.Aliyun)
	if err != nil {
		log.WithFields(log.Fields{
			"error": err.Error(),
		}).Error()
		return
	}
	content := QXCEncode(fmt.Sprintf("%s.sql", node.Name), []byte(result.MustGet()))
	err = aliyun.PutObject(key, content)
	if err != nil {
		log.WithFields(log.Fields{
			"error": err.Error(),
			"key":   key,
		}).Error()
		return
	}
}

func ListBackup(prefix string) {
	aliyun, err := NewAliyunBucketWithConfig(config.Aliyun)
	if err != nil {
		log.WithFields(log.Fields{
			"error": err.Error(),
		}).Error()
		return
	}
	objs, err := aliyun.ListObjects(prefix)
	if err != nil {
		log.WithFields(log.Fields{
			"error": err.Error(),
		}).Error()
		return
	}
	thead := []string{"key", "size", "last_modified"}
	tbody := []TableRow{}
	tbody = lo.Reduce(objs, func(agg []TableRow, item Object, index int) []TableRow {
		return append(agg, TableRow{
			"key":           item.Key,
			"size":          fmt.Sprintf("%d", item.Size),
			"last_modified": item.LastModified.Format("2006-01-02 15:04:05.000-07:00"),
		})
	}, tbody)
	OutputTable(thead, tbody)
}

func GetBackup(key string) {
	aliyun, err := NewAliyunBucketWithConfig(config.Aliyun)
	if err != nil {
		log.WithFields(log.Fields{
			"error": err.Error(),
		}).Error()
		return
	}
	body, err := aliyun.GetObject(key)
	if err != nil {
		log.WithFields(log.Fields{
			"error": err.Error(),
		}).Error()
		return
	}
	fname, content := QXCDecode(body)
	fmt.Printf("-- key: %s\n", key)
	fmt.Printf("-- filename: %s\n", fname)
	fmt.Printf("%s", string(content))
}

func main() {
	if len(os.Args) == 3 {
		if os.Args[1] == "transform" {
			TransformConfigure(os.Args[2])
			return
		}
	}
	err := LoadConfigure(`config.json`, &config)
	if err != nil {
		log.WithFields(log.Fields{
			"version": version,
		}).Panic("read configure fail")
	}
	e := echo.New()
	e.HideBanner = true
	e.HidePort = true
	e.Use(middleware.RecoverWithConfig(middleware.RecoverConfig{
		LogErrorFunc: func(c echo.Context, err error, stack []byte) error {
			msg := fmt.Sprintf("[PANIC RECOVER] %v %s\n", err, stack)
			log.WithFields(log.Fields{
				"error": err.Error(),
			}).Error(msg)
			return nil
		},
	}))
	e.Use(middleware.Gzip())
	e.Use(middleware.RequestLoggerWithConfig(middleware.RequestLoggerConfig{
		LogStatus:    true,
		LogURI:       true,
		LogMethod:    true,
		LogLatency:   true,
		LogUserAgent: true,
		LogError:     true,
		LogRemoteIP:  true,
		LogValuesFunc: func(c echo.Context, values middleware.RequestLoggerValues) error {
			ctx := log.WithFields(log.Fields{
				"uri":       values.URI,
				"status":    values.Status,
				"method":    values.Method,
				"latency":   values.Latency.String(),
				"agent":     values.UserAgent,
				"remote_ip": values.RemoteIP,
			})
			if values.Error != nil {
				ctx.Error(values.Error.Error())
			} else {
				ctx.Info()
			}
			return nil
		},
	}))
	WatchConfigureChange(`config.json`, func(c Configure, err error) {
		if err != nil {
			log.WithFields(log.Fields{
				"filename": `config.json`,
				"error":    err.Error(),
			}).Error("reload configure file fail")
			return
		}
		config.Database = c.Database
		config.Aliyun = c.Aliyun
	})
	if len(os.Args) == 2 {
		if os.Args[1] == "help" {
			fmt.Printf("%s Usage:\n", os.Args[0])
			fmt.Printf("    list <key-prefix>\n")
			fmt.Printf("    get  <key>\n")
			return
		}
	} else if len(os.Args) == 3 {
		if os.Args[1] == "list" {
			ListBackup(os.Args[2])
			return
		} else if os.Args[1] == "get" {
			GetBackup(os.Args[2])
			return
		}
	}
	log.WithFields(log.Fields{
		"version": version,
		"pid":     os.Getpid(),
	}).Info("server started")
	go func() {
		last_backup := time.Unix(0, 0)
		t := time.NewTimer(3 * time.Second)
		for range t.C {
			now := time.Now()
			if now.Hour() == 4 && last_backup.Day() != now.Day() {
				for _, ele := range config.Database {
					BackupMysql(ele)
				}
				last_backup = now
			}
			t.Reset(3 * time.Minute)
		}
	}()
	e.Logger.Fatal(e.Start(":0"))
}
